package org.juiceshop.musi.midi;

import java.io.*;

import javax.sound.midi.*;

import org.juiceshop.musi.midi.MidiSequence.ChannelEvent;
import org.juiceshop.musi.midi.MidiSequence.Event;
import org.juiceshop.musi.midi.MidiSequence.MetaEvent;

public class SimplePlayer {

	// Midi meta event
	static final int END_OF_TRACK_MESSAGE = 47;

	private Sequencer _sequencer;
	private Thread _sleepThread;

	/**
	 * Creates a new MidiPlayer object.
	 * 
	 * @throws MidiUnavailableException
	 */
	public SimplePlayer() {
		try {
			_sequencer = MidiSystem.getSequencer();
			_sequencer.open();
		} catch (MidiUnavailableException e) {
			throw new MidiException(e);
		}
//		MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();  
//		for (int i = 0; i < info.length; i++)  {
//			MidiDevice.Info inf = info[i];
//			MidiDevice mdev;
//			try {
//				mdev = MidiSystem.getMidiDevice(inf);
//			} catch (MidiUnavailableException e) {
//				throw new MidiException(e);
//			}
//			System.out.println(inf.toString()+" - "+(mdev.isOpen()?"open":"closed"));  		
//		}
			
		_sequencer.addMetaEventListener(new MetaEventListener() {
			@Override
			public void meta(MetaMessage event) {
				if (event.getType() == END_OF_TRACK_MESSAGE
						&& _sleepThread != null) {
					_sleepThread.interrupt();
				}
			}
		});
	}

	/**
	 * Loads a sequence from the file system.
	 * 
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 * @throws FileNotFoundException
	 */
	public Sequence loadSequence(String filename) {
		try {
			return loadSequence(new FileInputStream(filename));
		} catch (IOException e) {
			throw new MidiException(e);
		}
	}
	
	public Sequence loadSequence (byte[] byteSequence) {
		ByteArrayInputStream bais = new ByteArrayInputStream(byteSequence);
		return loadSequence(bais);
	}

	/**
	 * Loads a sequence from an input stream.
	 * 
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 */
	public Sequence loadSequence(InputStream is) {
		if (!is.markSupported()) {
			is = new BufferedInputStream(is);
		}
		Sequence sequence;
		try {
			sequence = MidiSystem.getSequence(is);
			is.close();
		} catch (InvalidMidiDataException e) {
			throw new MidiException(e);
		} catch (IOException e) {
			throw new MidiException(e);
		}
		return sequence;
	}
	

	/**
	 * Plays a sequence. This method returns immediately. The sequence is not
	 * played if it is invalid.
	 * 
	 * @throws InvalidMidiDataException
	 */
	public void play(Sequence sequence) {
		assert sequence != null;
		assert _sequencer != null && _sequencer.isOpen();
		try {
			_sequencer.setSequence(sequence);
		} catch (InvalidMidiDataException e) {
			throw new MidiException(e);
		}
		_sequencer.start();
		_sleepThread = Thread.currentThread();
		while (isPlaying()) {
			System.out.print("<"+_sequencer.getMicrosecondPosition()/1000+">");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException ex) {
				//
			}
		}
//		try {
//			Thread.sleep(Long.MAX_VALUE);
//		} catch (InterruptedException ex) {
//			//
//		}
		_sleepThread = null;
	}

	public void play(MidiSequence midiSequence) {
		play(createSequence(midiSequence));
	}
	
	public void play (byte[] midiBytes) {
		Sequence seq = loadSequence(midiBytes);
		play(seq);
	}
	
	/**
	 * Stops the sequencer and resets its position to 0.
	 */
	void stop() {
		assert _sequencer != null && _sequencer.isOpen();
		_sequencer.stop();
		_sequencer.setMicrosecondPosition(0);
	}

	/**
	 * Closes the sequencer.
	 */
	void close() {
		if (_sequencer != null && _sequencer.isOpen()) {
			_sequencer.close();
		}
	}

	boolean isPlaying() {
		return _sequencer.isRunning();
	}
	
	
	private static Sequence createSequence(MidiSequence midiSequence) {
		Sequence seq;
		try {
			seq = new Sequence(Sequence.PPQ, midiSequence.getResolution());
			Track track = seq.createTrack();
			for (Event ev : midiSequence) {
				MidiMessage midiMessage;
				if (ev instanceof ChannelEvent) {
					ShortMessage sm = new ShortMessage();
					ChannelEvent cev = (ChannelEvent)ev;
					sm.setMessage(cev.type.getNumber()<<4, cev.channel, cev.param1, cev.param2);
					midiMessage = sm;
				} else if (ev instanceof MetaEvent) {
					MetaMessage mm = new MetaMessage();
					MetaEvent mev = (MetaEvent)ev;
					mm.setMessage(mev.type.getNumber(), mev.data, mev.data.length);
					midiMessage = mm;
				} else {
					throw new Error("Unexpected event type: "+ev);
				}
				track.add(new MidiEvent(midiMessage,ev.tick));
			}
		} catch (InvalidMidiDataException e) {
			throw new Error(e);
		}
		return seq;
		
	}

}
